package game.square;

import game.dungeon.position.Direction;
import java.util.*;
import be.kuleuven.cs.som.annotate.*;

/**
 * An abstract class of borders that borders one or two squares.
 * 
 * @invar 	.. 
 * 		  | hasProperSquares()
 * 
 * @version 1.0
 * @author Chris Desomer, Geert Heyman
 */
public abstract class Border {

	
	/**
	 * Initialize a new border with the given booleans
	 * whether the new border is openable and slippery (or not).
	 * 
	 * @param	square
	 * 			The square this new border borders.
	 * @param	openable
	 * 			Indicates if this border is openable or not.
	 * @param	slippery
	 * 			Indicates if this border is slippery or not.
	 * @effect	the new state of this border is created
	 * 		  | new.isCreated() == true;
	 * @post	..
	 * 		  | new.isOpenable() == openable
	 * @post	..
	 * 		  | new.isSlippery() == slippery
	 */
	@Raw
	public Border(boolean slippery) throws IllegalArgumentException,
			IllegalStateException {
		this.slippery = slippery;
		setState(State.CREATED);
	}
	
	
	/**
	 * Enumeration of all possible states of a border: PLACED means
	 * this border is referring to at least one square, TERMINATED means
	 * this border should no longer be used. CREATED is the initial state
	 * of a border.
	 */
	private enum State {
		CREATED, PLACED, TERMINATED;
	}
	/**
	 * Return the state of this border.
	 */
	@Basic
	public State getState(){
		return state;
	}
	
	/**
	 * Check if this border is created
	 * 
	 * @return 	..
	 * 		  | getState() == State.CREATED
	 */
	public boolean isCreated(){
		return state == State.CREATED;
	}
	
	/**
	 * Check if this border is terminated
	 * 
	 * @return 	..
	 * 		  | getState() == State.TERMINATED
	 */
	public boolean isTerminated(){
		return state==State.TERMINATED;
	}
	
	/**
	 * Check if this border is placed.
	 * 
	 * @return	...
	 * 		  | getState() == State.PLACED
	 */
	public boolean isPlaced(){
		return state == State.PLACED;
	}
	
	/**
	 * Set the state of this border
	 * 
	 * @param	state
	 * 			The new state for this border
	 * @post	...
	 * 		  | if (state != null && !isTerminated())
	 * 		  |		then new.getState() == state
	 */
	public void setState(State state){
		if (state != null && !isTerminated())
			this.state=state;
	}
	
	/**
	 * Terminate this border
	 * 
	 * @pre		The squares referenced by this border don't reference
	 * 			this border (anymore)
	 * @post	This border is terminated.
	 * 		  | new.isTerminated() == true
	 * @post	The squares referenced by this border don't reference
	 * 			this border.
	 * @post	This border doesn't reference any squares anymore.
	 */
	public void terminate(){
		if (squares[1] != null)
			assert (!squares[1].hasAsBorder(this));
		if (squares[2] != null)
			assert (!squares[2].hasAsBorder(this));
		squares[1] = null;
		squares[2] = null;
		setState(State.TERMINATED);
		
		
	}
	
	/**
	 * Variable that stores the state of this border.
	 */
	private State state;
	/**
	 * Return a copy of the squares of this border.
	 * 
	 * @note : this method isn't annotated immutable because
	 * the reference of the returned array will be different
	 * every time you call this method.
	 */
	@Basic
	public Square[] getBorderedSquares() {
		return squares.clone();
	}
	
	/**
	 * Add the given square to the squares this border borders.
	 * 
	 * @post	..
	 * 		  | if (getSquares()[1] == null && getSquares()[2].getDirectionOf(this) == square
					.getDirectionOf(this).getOppositeDirection())
	 * 		  |		then new.getSquares()[1] == square
	 * @post	..
	 * 		  | else if (getSquares()[2] == null && getSquares[1].getDirectionOf(this) == square
					.getDirectionOf(this).getOppositeDirection())
	 * 		  |		then new.getSquares()[2] == square
	 * @post	..
	 * 		  | this.isPlaced() == true;
	 * @throws	IllegalStateException
	 * 			..
	 * 		  | !square.hasAsBorder(this)
	 * @throws	IllegalArgumentException
	 * 			..
	 * 		  |	!canHaveAsSquare(square)
	 */
	void addBorderedSquare (SquareImpl square) throws IllegalStateException,IllegalArgumentException {
		if (!square.hasAsBorder(this))
			throw new IllegalStateException("The given square must have a reference to this border first");
		if (!canHaveAsSquare(square))
			throw new IllegalArgumentException("This border can't have the given square as one of its squares.");
		addIfCompatibleDirections(square);
		setState(State.PLACED);
	}
	
	/*
	 * Add the given square to the squares this border borders
	 * if the direction of this border in the given square
	 * is compatible with the direction the other square
	 * this border borders. If there is no other square
	 * the square is added.
	 */
	private void addIfCompatibleDirections(SquareImpl square) {
		try {
			if (squares[1] == null&& squares[2].getDirectionOf(this) == square
					.getDirectionOf(this).getOppositeDirection())
				squares[1]= square;
			else if (squares[2] == null && squares[1].getDirectionOf(this) == square
					.getDirectionOf(this).getOppositeDirection()){
				squares[2]= square;
			}
			else {
				throw new IllegalStateException(
						"This border already borders two squares or the directions don't match");
			}
		} catch (NullPointerException e){
			// this border, borders no squares yet.
			squares[1] = square;	
		}
	}
	
	/**
	 * Remove the given square from this border.
	 * 
	 * @param 	square
	 * 			The square to remove.
	 * @post	If the square is bordered by this border, the square
	 * 			is removed from this border.
	 * @throws	IllegalArgumentException
	 * 			The given square isn't one of the squares this border
	 * 			borders.
	 */
	void removeBorderedSquare (Square square) throws IllegalArgumentException {
		if (squares[1] == square && squares[2] != square)
			throw new IllegalArgumentException(
			"The given square isn't bordered by this border");
		if (squares[1] == square)
			squares[1] = null;
		if (squares[2] == square )
			squares[2] = null;
		}
	
	/**
	 * Check if this border is a border of the given square.
	 * 
	 * @param 	square
	 * 			The square to check with.
	 * @return	True if and only if the square is one of the two squares bordered
	 * 			by this border, the given square can be non effective if this border
	 * 			has a non effective square (it borders no more than one square) this
	 * 			method return true false otherwise.
	 */
	public boolean isBorderOf (Square square){
		if (square != squares[1] && (square != squares[2]))
			return false;
		return true;
	}
	
	/**
	 * Return the square who shares this border with the given square.
	 * 
	 * @param 	square
	 * 			The square to get the neighboring square along this border
	 * 			from.
	 * @pre		isBorderOf(square)
	 * @return	The neighboring square along this border, if 
	 * 			there is no neighboring square, the result is the null
	 * 			reference.
	 */
	public SquareImpl getNeighborSquareFrom (Square square){
		assert (isBorderOf(square));
		if (square == squares[1]){
			return squares[2];
		}
		return squares[1];	
	}

	/**
	 * Check if this border can have the given square as one 
	 * of the squares it borders.
	 * 
	 * @param 	square
	 * 			The square to check.	
	 * @return	True if the given square is null.
	 * @result	Otherwise, false if the given square or this border are 
	 * 			terminated.
	 * @result	Otherwise the result is true.	
	 */
	public boolean canHaveAsSquare(Square square) {
		if (square == null)
			return true;
		if (square.isTerminated()|| isTerminated())
			return false;
		return true;
	}

	/**
	 * Check if this border has proper squares.
	 * 
	 * @return 	False if this border can't have one of the squares
	 * 			as its square.
	 * @result	Otherwise, false if at least one of the squares doesn't keep
	 * 			a reference to this border in the corresponding the direction:
	 * 			the direction of the first square of the given squares corresponds
	 * 			with the direction of this border
	 * @result	Otherwise, True if and only if all of the conditions above are false.
	 */
	@Raw
	public boolean hasProperSquares() {
		if (!canHaveAsSquare(squares[1]) || !canHaveAsSquare(squares[2]))
			return false;
		try {
			Direction directionInRefToSquare1 = squares[1].getDirectionOf(this);
			Direction directionInRefToSquare2 = squares[2].getDirectionOf(this);
			if (!(directionInRefToSquare2 == directionInRefToSquare1.getOppositeDirection()))
				return false;
			if (squares[1].getBorderInDirection(directionInRefToSquare1) != this
					|| squares[2].getBorderInDirection(directionInRefToSquare2) != this)
				return false;
			return true;
		}
		catch (NullPointerException e){
			// it is allowed that one of the squares is not effective.
			if (squares[1] == null && !(squares[2] == null))
				return (squares[2].getBorderInDirection(squares[2].getDirectionOf(this)) == this);
			if (squares[2] == null && !(squares[1] == null))
				return (squares[1].getBorderInDirection(squares[1].getDirectionOf(this)) == this);
			// if both squares are not effective this border isn't placed yet/anymore.
			return !isPlaced();
		}		
	}
		
	/**
	 * Variable that references the two squares this border
	 * borders.
	 */
	private final SquareImpl[] squares = new SquareImpl[3];
	
	/**
	 * Check if this border is an open border: the bordering squares
	 * share temperature and humidity.
	 * 
	 * @result	...
	 * 		  | if (!isOpenable())
	 * 		  |		then result == false
	 * @note	The rest of the documentation should be 
	 * 			checked in the subclasses.
	 */
	public abstract boolean isOpenBorder();
	
	/**
	 *  Check if this border is openable.
	 */
	@Basic
	@Immutable
	public abstract boolean isOpenable();
	
	
	/**
	 * Check if this border is slippery.
	 */
	@Basic
	@Immutable
	public boolean isSlippery() {
		return slippery;
	}
	/**
	 * Variable that stores if this border is slippery or not.
	 */
	private final boolean slippery;

	/**
	 * Check which border is stronger (has a higher priority when
	 * two squares become neighbors)
	 * @param 	border
	 * 			the border to check the strength with
	 * @return	...
	 * 		  | getMergeOrderNbs.get(this.getClass()) <= getMergeOrderNbs.get(border
	 *		  |		.getClass())
	 */
	public boolean isStrongerThan(Border border){
		return MERGE_ORDER_NBS.get(this.getClass()) <= MERGE_ORDER_NBS.get(border
				.getClass());
	}
	/**
	 * Return the merge order numbers of borders.
	 */
	@Basic
	@Immutable
	public static Map<Class<? extends Border>,Integer> getMergeOrdersNbs(){
		return new HashMap<Class<? extends Border>,Integer>(MERGE_ORDER_NBS);
	}
	
	private static final Map<Class<? extends Border>,Integer> MERGE_ORDER_NBS= new HashMap<Class<? extends Border>, Integer>();
	static {
		// all not abstract objects should get a merge order here.
		MERGE_ORDER_NBS.put(Wall.class,1);
		MERGE_ORDER_NBS.put(Door.class,2);
		MERGE_ORDER_NBS.put(NoPhysicalBorder.class,3);
	}
	
	/**
	 * Get newly created border (it borders no squares), that is
	 * on all other aspects equal to this border.
	 *  
	 * @return	..
	 * 		  |	result.isCreated() && result.getClass() == getClass()
	 */
	public abstract Border getNewEqualBorder();
}
